# -*- coding: utf-8 -*-
import os
import random
import numpy
import pandas as pd
from csv import DictWriter
from deap import base, creator, tools
from timeit import default_timer as timer
import multiprocessing

df_artisans = pd.read_csv('data/sample/artisans.csv')
df_orders = pd.read_csv('data/sample/orders.csv')
df_times = pd.read_csv('data/sample/travel_times.csv')

artisan_orders = dict()

# 定义基因编号长度为手艺人数量
genSize = len(df_artisans)
# 定义基因长度为订单总数
indSize = len(df_orders)
small_constant = 0.000001

# Create Fitness and Individual Classes
creator.create('FitnessMax', base.Fitness, weights=(1.0,))
creator.create('Individual', list, fitness=creator.FitnessMax)
toolbox = base.Toolbox()


def get_near_time(p1, p2, start):
    try:
        travel_times = df_times[(df_times['p1'] == p1) & (df_times['p2'] == p2)]
        travel_times['start_time'] = travel_times['start_time'].apply(lambda x: int(x) - int(start))
        best_time = travel_times['start_time'].abs().sort_values()[0]
    except:
        best_time = 0
    return best_time


def genes_to_time(individual):
    """
    基因转换为消耗时间
    :param individual:
    :return:
    """
    global df_artisans, df_orders, df_times
    error_routes = False
    total_time = 0
    # 检查时间窗口是否满足
    print('check gene: ' + str(individual[0:20]) + '...')
    for order_id, artisan_id in enumerate(individual):
        travel_time = 0
        if artisan_id in artisan_orders:
            pre_order_id = artisan_orders[artisan_id][-1]
            pre_order = df_orders.iloc[pre_order_id]
        else:
            # 如果是该手艺人第一单
            pre_order = df_artisans.iloc[(artisan_id-1)]
        pre_time = pre_order['stop']
        now_order = df_orders.iloc[order_id]
        if pre_order is not None:
            # 排除预约时间有冲突的情况
            try:
                now_start = int(now_order['start'])
            except:
                now_start = 0
            if now_start < pre_time:
                error_routes = True
                break
            # 排除路程时间不够的情况
            try:
                p1 = str(pre_order['lon']) + ',' + str(pre_order['lat'])
                p2 = str(now_order['lon']) + ',' + str(now_order['lat'])
                start = now_order['start'] - 60
                interval_time = int(now_order['start']) - int(pre_order['stop'])
                travel_time = get_near_time(p1, p2, start)
            except:
                interval_time = 0
                travel_time = 0
            if travel_time > interval_time:
                error_routes = True
                break
        try:
            service_time = int(now_order['stop']) - int(now_order['start'])
        except:
            service_time = 0
        total_time += (travel_time + service_time)
        if artisan_id in artisan_orders:
            artisan_orders[artisan_id].append(order_id)
        else:
            artisan_orders[artisan_id] = [order_id]
    if error_routes:
        return None
    return total_time


def eval_vrptw(individual):
    """
    进化过程
    :param individual: 个体对象
    :param instance: 个体相关数据
    :return:
    """
    global df_points
    total_time = genes_to_time(individual)
    if total_time is not None:
        fitness = 1.0 / total_time
    else:
        fitness = 0
    return fitness,


def ga_vrptw(ind_size, pop_size, cx_pb, mut_pb, gen_size, exportCSV=False):
    """
    遗传算法主程序
    :param ind_size: 基因大小
    :param pop_size: 种群数量
    :param cx_pb: 交叉概率
    :param mut_pb: 变异概率
    :param gen_size: 进化次数
    :param exportCSV:
    :return:
    """
    # 生成基因序列
    # 从手艺人编号中随机选取分派给订单
    toolbox.register('indexes', random.choices, range(1, genSize + 1), k=ind_size)
    # Structure initializers
    toolbox.register('individual', tools.initIterate, creator.Individual, toolbox.indexes)
    # 生成初始的种群
    toolbox.register('population', tools.initRepeat, list, toolbox.individual)
    # Initialize the population
    pop = toolbox.population(n=pop_size)
    # Operator registering
    toolbox.register('evaluate', eval_vrptw)
    toolbox.register('select', tools.selRoulette)
    # toolbox.register('mate', cx_partialy_matched)
    # toolbox.register('mutate', mut_inverse_indexes)
    toolbox.register('mate', tools.cxPartialyMatched)
    # 每个基因变异的概率
    toolbox.register('mutate', tools.mutShuffleIndexes, indpb=mut_pb)
    # Results holders for exporting results to CSV file
    csvData = []
    print('start evolution')
    # Evaluate the entire population
    fitnesses = list(toolbox.map(toolbox.evaluate, pop))
    for ind, fit in zip(pop, fitnesses):
        ind.fitness.values = fit
    # Debug, suppress print()
    # print('  Evaluated %d individuals' % len(pop)
    fits = [ind.fitness.values[0] for ind in pop]
    # Begin the evolution
    g = 0
    while max(fits) <= 0 and g < gen_size:
        # A new generation
        g = g + 1
        print("-- Generation %i --" % g)
        # Select the next generation individuals
        offspring = toolbox.select(pop, len(pop))
        # Clone the selected individuals
        offspring = list(map(toolbox.clone, offspring))
        # Apply crossover and mutation on the offspring
        for child1, child2 in zip(offspring[::2], offspring[1::2]):
            # cross two individuals with probability CXPB
            if random.random() < cx_pb:
                toolbox.mate(child1, child2)
                # fitness values of the children
                # must be recalculated later
                del child1.fitness.values
                del child2.fitness.values
        for mutant in offspring:
            # mutate an individual with probability MUTPB
            if random.random() < mut_pb:
                toolbox.mutate(mutant)
                del mutant.fitness.values
        # Evaluate the individuals with an invalid fitness
        invalid_ind = [ind for ind in offspring if not ind.fitness.valid]
        fitnesses = map(toolbox.evaluate, invalid_ind)
        for ind, fit in zip(invalid_ind, fitnesses):
            ind.fitness.values = fit
        print("  Evaluated %i individuals" % len(invalid_ind))
        # The population is entirely replaced by the offspring
        pop[:] = offspring
        # Gather all the fitnesses in one list and print the stats
        fits = [ind.fitness.values[0] for ind in pop]
        length = len(pop)
        if length != 0:
            mean = sum(fits) / length
            sum2 = sum(x * x for x in fits)
            std = abs(sum2 / length - mean ** 2) ** 0.5
            print("  Min %s" % min(fits))
            print("  Max %s" % max(fits))
            print("  Avg %s" % mean)
            print("  Std %s" % std)
    print("-- End of (successful) evolution --")
    best_ind = tools.selBest(pop, 1)[0]
    print("Best individual is %s, %s" % (best_ind, best_ind.fitness.values))
    if exportCSV:
        csvFilename = 'pS%s_cP%s_mP%s_nG%s.csv' % (pop_size, cx_pb, mut_pb, gen_size)
        csvPathname = os.path.join('results', csvFilename)
        print('Write to file: %s' % csvPathname)
        utils.makeDirsForFile(pathname=csvPathname)
        if not utils.exist(pathname=csvPathname, overwrite=True):
            with open(csvPathname, 'w') as f:
                fieldnames = ['generation', 'evaluated_individuals', 'min_fitness', 'max_fitness', 'avg_fitness',
                              'std_fitness', 'avg_cost']
                writer = DictWriter(f, fieldnames=fieldnames, dialect='excel')
                writer.writeheader()
                for csvRow in csvData:
                    writer.writerow(csvRow)
    return best_ind


def main():
    random.seed(73)
    popSize = 6000
    cxPb = 0.9
    mutPb = 0.05
    NGen = 1000
    exportCSV = True
    # Global creation of the individuals for GA
    ga_vrptw(
        ind_size=indSize,
        pop_size=popSize,
        cx_pb=cxPb,
        mut_pb=mutPb,
        gen_size=NGen,
        exportCSV=exportCSV
    )


if __name__ == '__main__':
    if __package__ is None:
        import sys
        from os import path
        sys.path.append(path.dirname(path.dirname(path.abspath(__file__))))
        from gavrptw import utils
    else:
        from .gavrptw import utils
    pool = multiprocessing.Pool(4)
    toolbox.register("map", pool.map)
    tic = timer()
    main()
    print('Computing Time: %s' % (timer() - tic))
    pool.close()
